/*
 * $Id: FacesSetupInterceptor.java 651946 2008-04-27 13:41:38Z apetrelli $
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package haaproject.webwork.jsf;

import haaproject.webwork.PluginException;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

import javax.faces.FactoryFinder;
import javax.faces.application.Application;
import javax.faces.application.ApplicationFactory;
import javax.faces.application.NavigationHandler;
import javax.faces.application.StateManager;
import javax.faces.application.ViewHandler;
import javax.faces.context.FacesContext;
import javax.faces.context.FacesContextFactory;
import javax.faces.el.PropertyResolver;
import javax.faces.el.VariableResolver;
import javax.faces.event.ActionListener;
import javax.faces.lifecycle.Lifecycle;
import javax.faces.lifecycle.LifecycleFactory;

import com.opensymphony.webwork.ServletActionContext;
import com.opensymphony.webwork.util.ClassLoaderUtils;
import com.opensymphony.xwork.Action;
import com.opensymphony.xwork.ActionInvocation;
import com.opensymphony.xwork.config.entities.ActionConfig;
import com.opensymphony.xwork.config.entities.ResultConfig;
import com.opensymphony.xwork.interceptor.Interceptor;

/**
 * * Initializes the JSF context for this request.
 * <p>
 * </P>
 * The JSF Application can additionaly be configured from the Struts.xml by adding &lt;param&gt; tags to the jsfSetup
 * &lt;interceptor-ref&gt;.
 * <p>
 * </p>
 * <b>Example struts.xml configuration:</b>
 * 
 * <pre>
 *   &lt;interceptor-ref name=&quot;jsfSetup&quot;&gt;
 *       &lt;param name=&quot;actionListener&quot;&gt;&lt;/param&gt;
 *       &lt;param name=&quot;defaultRenderKitId&quot;&gt;&lt;/param&gt;
 *       &lt;param name=&quot;supportedLocale&quot;&gt;&lt;/param&gt;
 *       &lt;param name=&quot;defaultLocale&quot;&gt;&lt;/param&gt;
 *       &lt;param name=&quot;messageBundle&quot;&gt;&lt;/param&gt;
 *       &lt;param name=&quot;navigationHandler&quot;&gt;org.apache.struts2.jsf.StrutsNavigationHandler&lt;/param&gt;
 *       &lt;param name=&quot;propertyResolver&quot;&gt;&lt;/param&gt;
 *       &lt;param name=&quot;stateManager&quot;&gt;&lt;/param&gt;
 *       &lt;param name=&quot;variableResolver&quot;&gt;
 *           org.apache.myfaces.el.VariableResolverImpl
 *          ,org.apache.struts2.jsf.StrutsVariableResolver
 *       &lt;/param&gt;
 *       &lt;param name=&quot;viewHandler;&quot;&gt;org.apache.shale.tiles.TilesViewHandler&lt;/param&gt;
 *   &lt;/interceptor-ref&gt;
 * </pre>
 * 
 * <p>
 * </p>
 * <b>Note: None of the parameters are required but all are shown in the example for completeness.</b>
 */
public class FacesSetupInterceptor extends FacesSupport implements Interceptor {

	private static final long serialVersionUID = -621512342655103941L;

	private String lifecycleId = LifecycleFactory.DEFAULT_LIFECYCLE;

	private FacesContextFactory facesContextFactory;

	private Lifecycle lifecycle;

	// jsf Application configuration
	private List<String> actionListener;

	private String defaultRenderKitId;

	private List<String> supportedLocale;

	private String defaultLocale;

	private String messageBundle;

	private List<String> navigationHandler;

	private List<String> propertyResolver;

	private List<String> stateManager;

	private List<String> variableResolver;

	private List<String> viewHandler;

	/**
	 * Sets the lifecycle id
	 * 
	 * @param id
	 *            The id
	 */
	public void setLifecycleId(String id) {
		this.lifecycleId = id;
	}

	/**
	 * Initializes the lifecycle and factories
	 */
	public void init() {
		try {
			facesContextFactory = (FacesContextFactory) FactoryFinder.getFactory(FactoryFinder.FACES_CONTEXT_FACTORY);
		} catch (Exception ex) {
			log.debug("Unable to initialize faces", ex);
			return;
		}

		// Javadoc says: Lifecycle instance is shared across multiple
		// simultaneous requests, it must be implemented in a thread-safe
		// manner.
		// So we can acquire it here once:
		LifecycleFactory lifecycleFactory = (LifecycleFactory) FactoryFinder
				.getFactory(FactoryFinder.LIFECYCLE_FACTORY);
		lifecycle = lifecycleFactory.getLifecycle(lifecycleId);

		Application application = ((ApplicationFactory) FactoryFinder.getFactory(FactoryFinder.APPLICATION_FACTORY))
				.getApplication();

		if (actionListener != null) {
			Iterator i = actionListener.iterator();
			application.setActionListener((ActionListener) getApplicationObject(ActionListener.class, i, application
					.getActionListener()));
		}
		if (defaultRenderKitId != null && defaultRenderKitId.length() > 0) {
			application.setDefaultRenderKitId(defaultRenderKitId);
		}
		if (messageBundle != null && messageBundle.length() > 0) {
			application.setMessageBundle(messageBundle);
		}
		if (supportedLocale != null) {
			List<Locale> locales = new ArrayList<Locale>();
			for (Iterator i = supportedLocale.iterator(); i.hasNext();) {
				locales.add(toLocale((String) i.next()));
			}
			application.setSupportedLocales(locales);
		}
		if (defaultLocale != null && defaultLocale.length() > 0) {
			application.setDefaultLocale(toLocale(defaultLocale));
		}
		if (navigationHandler != null) {
			Iterator i = navigationHandler.iterator();
			application.setNavigationHandler((NavigationHandler) getApplicationObject(NavigationHandler.class, i,
					application.getNavigationHandler()));
		}
		if (propertyResolver != null) {
			Iterator i = propertyResolver.iterator();
			application.setPropertyResolver((PropertyResolver) getApplicationObject(PropertyResolver.class, i,
					application.getPropertyResolver()));
		}
		if (stateManager != null) {
			Iterator i = stateManager.iterator();
			application.setStateManager((StateManager) getApplicationObject(StateManager.class, i, application
					.getStateManager()));
		}
		if (variableResolver != null) {
			Iterator i = variableResolver.iterator();
			application.setVariableResolver((VariableResolver) getApplicationObject(VariableResolver.class, i,
					application.getVariableResolver()));
		}
		if (viewHandler != null) {
			Iterator i = viewHandler.iterator();
			application.setViewHandler((ViewHandler) getApplicationObject(ViewHandler.class, i, application
					.getViewHandler()));
		}
	}

	/**
	 * Creates the faces context for other phases.
	 * 
	 * @param invocation
	 *            The action invocation
	 */
	public String intercept(ActionInvocation invocation) throws Exception {
		if (facesContextFactory != null) {
			if (isFacesAction(invocation)) {

				invocation.getInvocationContext().put(FacesInterceptor.FACES_ENABLED, Boolean.TRUE);

				FacesContext facesContext = facesContextFactory.getFacesContext(ServletActionContext
						.getServletContext(), ServletActionContext.getRequest(), ServletActionContext.getResponse(),
						lifecycle);

				setLifecycle(lifecycle);

				try {
					return invocation.invoke();
				} finally {
					facesContext.release();
				}
			}
		} else {
			throw new PluginException(
					"Unable to initialize jsf interceptors probably due missing JSF implementation libraries",
					invocation.getProxy().getConfig());
		}
		return invocation.invoke();
	}

	/**
	 * Cleans up the lifecycle and factories
	 */
	public void destroy() {
		facesContextFactory = null;
		lifecycle = null;
	}

	/**
	 * Determines if this action mapping will be have a JSF view
	 * 
	 * @param inv
	 *            The action invocation
	 * @return True if the JSF interceptors should fire
	 */
	protected boolean isFacesAction(ActionInvocation inv) {
		ActionConfig config = inv.getProxy().getConfig();
		if (config != null) {

			// TODO
			ResultConfig resultConfig = (ResultConfig) config.getResults().get(Action.SUCCESS);
			Class resClass = null;
			try {
				resClass = Class.forName(resultConfig.getClassName());
			} catch (ClassNotFoundException ex) {
				log.warn("Can't find result class, ignoring as a faces request", ex);
			}
			if (resClass != null) {
				if (resClass.isAssignableFrom(FacesResult.class)) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Constructs an object from a list of class names. This method supports creating the objects using constructor
	 * delegation, if the requested class supports it. Classes will be imbedded from top to bottom in the list with the
	 * last class listed being the one that will be returned.
	 * 
	 * @param interfaceClass
	 *            The Class type that is expected to be returned
	 * @param classNamesIterator
	 *            An Iterator for a list of Strings that represent the class names
	 * @param defaultObject
	 *            The current Object that the jsf Application has set
	 * @return
	 */
	private Object getApplicationObject(Class interfaceClass, Iterator classNamesIterator, Object defaultObject) {
		Object current = defaultObject;

		while (classNamesIterator.hasNext()) {
			String implClassName = (String) classNamesIterator.next();
			Class implClass = null;

			try {
				implClass = ClassLoaderUtils.loadClass(implClassName, this.getClass());
			} catch (ClassNotFoundException e1) {
				throw new IllegalArgumentException("Class " + implClassName + " was not found.");
			}

			// check, if class is of expected interface type
			if (!interfaceClass.isAssignableFrom(implClass)) {
				throw new IllegalArgumentException("Class " + implClassName + " is no " + interfaceClass.getName());
			}

			if (current == null) {
				// nothing to decorate
				try {
					current = implClass.newInstance();
				} catch (InstantiationException e) {
					log.error(e.getMessage(), e);
					throw new PluginException(e);
				} catch (IllegalAccessException e) {
					log.error(e.getMessage(), e);
					throw new PluginException(e);
				}
			} else {
				// let's check if class supports the decorator pattern
				try {
					Constructor delegationConstructor = implClass.getConstructor(new Class[] { interfaceClass });
					// impl class supports decorator pattern,
					try {
						// create new decorator wrapping current
						current = delegationConstructor.newInstance(new Object[] { current });
					} catch (InstantiationException e) {
						log.error(e.getMessage(), e);
						throw new PluginException(e);
					} catch (IllegalAccessException e) {
						log.error(e.getMessage(), e);
						throw new PluginException(e);
					} catch (InvocationTargetException e) {
						log.error(e.getMessage(), e);
						throw new PluginException(e);
					}
				} catch (NoSuchMethodException e) {
					// no decorator pattern support
					try {
						current = implClass.newInstance();
					} catch (InstantiationException e1) {
						log.error(e.getMessage(), e);
						throw new PluginException(e);
					} catch (IllegalAccessException e1) {
						log.error(e.getMessage(), e);
						throw new PluginException(e);
					}
				}
			}
		}

		return current;
	}

	/**
	 * Takes a comma delimited string of class names and stores the names in an <code>ArrayList</code>. The incoming
	 * <code>String</code> will be cleaned of any whitespace characters before the class names are stored.
	 * 
	 * @param actionListener
	 *            A comma delimited string of class names
	 */
	public void setActionListener(String actionListener) {
		if (this.actionListener == null) {
			this.actionListener = new ArrayList<String>();
		}
		String clean = actionListener.replaceAll("[ \t\r\n]", "");
		String[] actionListenerNames = clean.split(",");

		for (int i = 0; i < actionListenerNames.length; i++) {
			this.actionListener.add(actionListenerNames[i]);
		}
	}

	/**
	 * A <code>String</code> to be used as the defaultRenderKitId for the jsf application. The incoming
	 * <code>String</code> will be cleaned of whitespace characters.
	 * 
	 * @param defaultRenderKitId
	 *            The defaultRenderKitId
	 */
	public void setDefaultRenderKitId(String defaultRenderKitId) {
		String clean = defaultRenderKitId.replaceAll("[ \t\r\n]", "");
		this.defaultRenderKitId = clean;
	}

	/**
	 * Takes a comma delimited string of local names and stores the names in an <code>ArrayList</code>. The incoming
	 * <code>String</code> will be cleaned of any whitespace characters before the class names are stored.
	 * 
	 * @param supportedLocale
	 *            A comma delimited string of local names
	 */
	public void setSupportedLocale(String supportedLocale) {
		if (this.supportedLocale == null) {
			this.supportedLocale = new ArrayList<String>();
		}
		String clean = supportedLocale.replaceAll("[ \t\r\n]", "");
		String[] supportedLocaleNames = clean.split(",");

		for (int i = 0; i < supportedLocaleNames.length; i++) {
			this.supportedLocale.add(supportedLocaleNames[i]);
		}
	}

	/**
	 * Stores a String representation of the defaultLocale. The incoming <code>String</code> will be cleaned of any
	 * whitespace characters before the class names are stored.
	 * 
	 * @param defaultLocale
	 *            The default local
	 */
	public void setDefaultLocale(String defaultLocale) {
		String clean = defaultLocale.replaceAll("[ \t\r\n]", "");
		this.defaultLocale = clean;
	}

	/**
	 * Stores the messageBundle to be used to configure the jsf Application.
	 * 
	 * @param messageBundle
	 *            The messageBundle
	 */
	public void setMessageBundle(String messageBundle) {
		String clean = messageBundle.replaceAll("[ \t\r\n]", "");
		this.messageBundle = clean;
	}

	/**
	 * Takes a comma delimited string of class names and stores the names in an <code>ArrayList</code>. The incoming
	 * <code>String</code> will be cleaned of any whitespace characters before the class names are stored.
	 * 
	 * @param navigationHandlerName
	 *            A comma delimited string of class names
	 */
	public void setNavigationHandler(String navigationHandlerName) {
		if (navigationHandler == null) {
			navigationHandler = new ArrayList<String>();
		}
		String clean = navigationHandlerName.replaceAll("[ \t\r\n]", "");
		String[] navigationHandlerNames = clean.split(",");

		for (int i = 0; i < navigationHandlerNames.length; i++) {
			navigationHandler.add(navigationHandlerNames[i]);
		}
	}

	/**
	 * Takes a comma delimited string of class names and stores the names in an <code>ArrayList</code>. The incoming
	 * <code>String</code> will be cleaned of any whitespace characters before the class names are stored.
	 * 
	 * @param propertyResolverName
	 *            A comma delimited string of class names
	 */
	public void setPropertyResolver(String propertyResolverName) {
		if (propertyResolver == null) {
			propertyResolver = new ArrayList<String>();
		}
		String clean = propertyResolverName.replaceAll("[ \t\r\n]", "");
		String[] propertyResolverNames = clean.split(",");

		for (int i = 0; i < propertyResolverNames.length; i++) {
			propertyResolver.add(propertyResolverNames[i]);
		}
	}

	/**
	 * Takes a comma delimited string of class names and stores the names in an <code>ArrayList</code>. The incoming
	 * <code>String</code> will be cleaned of any whitespace characters before the class names are stored.
	 * 
	 * @param stateManagerName
	 *            A comma delimited string of class names
	 */
	public void setStateManager(String stateManagerName) {
		if (stateManager == null) {
			stateManager = new ArrayList<String>();
		}
		String clean = stateManagerName.replaceAll("[ \t\r\n]", "");
		String[] stateManagerNames = clean.split(",");

		for (int i = 0; i < stateManagerNames.length; i++) {
			stateManager.add(stateManagerNames[i]);
		}
	}

	/**
	 * Takes a comma delimited string of class names and stores the names in an <code>ArrayList</code>. The incoming
	 * <code>String</code> will be cleaned of any whitespace characters before the class names are stored.
	 * 
	 * @param variableResolverName
	 *            A comma delimited string of class names
	 */
	public void setVariableResolver(String variableResolverName) {
		if (variableResolver == null) {
			variableResolver = new ArrayList<String>();
		}
		String clean = variableResolverName.replaceAll("[ \t\r\n]", "");
		String[] variableResolverNames = clean.split(",");

		for (int i = 0; i < variableResolverNames.length; i++) {
			variableResolver.add(variableResolverNames[i]);
		}
	}

	/**
	 * Takes a comma delimited string of class names and stores the names in an <code>ArrayList</code>. The incoming
	 * <code>String</code> will be cleaned of any whitespace characters before the class names are stored.
	 * 
	 * @param viewHandlerName
	 *            A comma delimited string of class names
	 */
	public void setViewHandler(String viewHandlerName) {
		if (viewHandler == null) {
			viewHandler = new ArrayList<String>();
		}
		String[] viewHandlerNames = viewHandlerName.split(",^[ \t\r\n]+|[ \t\r\n]+$");

		for (int i = 0; i < viewHandlerNames.length; i++) {
			viewHandler.add(viewHandlerNames[i]);
		}
	}

	/**
	 * Converts a locale string to <code>Locale</code> class. Accepts both '_' and '-' as separators for locale
	 * components.
	 * 
	 * @param localeString
	 *            string representation of a locale
	 * @return Locale instance, compatible with the string representation
	 */
	private Locale toLocale(String localeString) {
		if ((localeString == null) || (localeString.length() == 0)) {
			Locale locale = Locale.getDefault();
			if (log.isWarnEnabled())
				log.warn("Locale name in faces-config.xml null or empty, setting locale to default locale : "
						+ locale.toString());
			return locale;
		}

		int separatorCountry = localeString.indexOf('_');
		char separator;
		if (separatorCountry >= 0) {
			separator = '_';
		} else {
			separatorCountry = localeString.indexOf('-');
			separator = '-';
		}

		String language, country, variant;
		if (separatorCountry < 0) {
			language = localeString;
			country = variant = "";
		} else {
			language = localeString.substring(0, separatorCountry);

			int separatorVariant = localeString.indexOf(separator, separatorCountry + 1);
			if (separatorVariant < 0) {
				country = localeString.substring(separatorCountry + 1);
				variant = "";
			} else {
				country = localeString.substring(separatorCountry + 1, separatorVariant);
				variant = localeString.substring(separatorVariant + 1);
			}
		}

		return new Locale(language, country, variant);
	}
}
